package com.baidu.iit.pxp.el.resolver;

import com.baidu.iit.pxp.el.*;
import com.baidu.iit.pxp.el.juel.MethodNotFoundException;
import com.baidu.iit.pxp.el.juel.PropertyNotFoundException;

import java.beans.FeatureDescriptor;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * User: huangweili
 * Date: 14-4-29
 * Time: 下午5:40
 */
public class BeanELResolver extends ELResolver {
    protected static final class BeanProperties {
        private final Map<String, BeanProperty> map = new HashMap<String, BeanProperty>();

        public BeanProperties(Class<?> baseClass) {
            PropertyDescriptor[] descriptors;
            try {
                descriptors = Introspector.getBeanInfo(baseClass).getPropertyDescriptors();
            } catch (IntrospectionException e) {
                throw new ELException(e);
            }
            for (PropertyDescriptor descriptor : descriptors) {
                map.put(descriptor.getName(), new BeanProperty(descriptor));
            }
        }

        public BeanProperty getBeanProperty(String property) {
            return map.get(property);
        }
    }

    protected static final class BeanProperty {
        private final PropertyDescriptor descriptor;

        public BeanProperty(PropertyDescriptor descriptor) {
            this.descriptor = descriptor;
        }

        public Class<?> getPropertyType() {
            return descriptor.getPropertyType();
        }

        public Method getReadMethod() {
            return findAccessibleMethod(descriptor.getReadMethod());
        }

        public Method getWriteMethod() {
            return findAccessibleMethod(descriptor.getWriteMethod());
        }

        public boolean isReadOnly() {
            return findAccessibleMethod(descriptor.getWriteMethod()) == null;
        }
    }

    private static Method findAccessibleMethod(Method method) {
        if (method == null || method.isAccessible()) {
            return method;
        }
        try {
            method.setAccessible(true);
        } catch (SecurityException e) {
            for (Class<?> cls : method.getDeclaringClass().getInterfaces()) {
                Method mth = null;
                try {
                    mth = cls.getMethod(method.getName(), method.getParameterTypes());
                    mth = findAccessibleMethod(mth);
                    if (mth != null) {
                        return mth;
                    }
                } catch (NoSuchMethodException ignore) {
                    // do nothing
                }
            }
            Class<?> cls = method.getDeclaringClass().getSuperclass();
            if (cls != null) {
                Method mth = null;
                try {
                    mth = cls.getMethod(method.getName(), method.getParameterTypes());
                    mth = findAccessibleMethod(mth);
                    if (mth != null) {
                        return mth;
                    }
                } catch (NoSuchMethodException ignore) {
                    // do nothing
                }
            }
            return null;
        }
        return method;
    }

    private final boolean readOnly;
    private final ConcurrentHashMap<Class<?>, BeanProperties> cache;

    private ExpressionFactory defaultFactory;


    public BeanELResolver() {
        this(false);
    }


    public BeanELResolver(boolean readOnly) {
        this.readOnly = readOnly;
        this.cache = new ConcurrentHashMap<Class<?>, BeanProperties>();
    }


    @Override
    public Class<?> getCommonPropertyType(ELContext context, Object base) {
        return isResolvable(base) ? Object.class : null;
    }


    @Override
    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
        if (isResolvable(base)) {
            final PropertyDescriptor[] properties;
            try {
                properties = Introspector.getBeanInfo(base.getClass()).getPropertyDescriptors();
            } catch (IntrospectionException e) {
                return Collections.<FeatureDescriptor>emptyList().iterator();
            }
            return new Iterator<FeatureDescriptor>() {
                int next = 0;

                public boolean hasNext() {
                    return properties != null && next < properties.length;
                }

                public FeatureDescriptor next() {
                    PropertyDescriptor property = properties[next++];
                    FeatureDescriptor feature = new FeatureDescriptor();
                    feature.setDisplayName(property.getDisplayName());
                    feature.setName(property.getName());
                    feature.setShortDescription(property.getShortDescription());
                    feature.setExpert(property.isExpert());
                    feature.setHidden(property.isHidden());
                    feature.setPreferred(property.isPreferred());
                    feature.setValue(TYPE, property.getPropertyType());
                    feature.setValue(RESOLVABLE_AT_DESIGN_TIME, true);
                    return feature;
                }

                public void remove() {
                    throw new UnsupportedOperationException("cannot remove");
                }
            };
        }
        return null;
    }


    @Override
    public Class<?> getType(ELContext context, Object base, Object property) {
        if (context == null) {
            throw new NullPointerException();
        }
        Class<?> result = null;
        if (isResolvable(base)) {
            result = toBeanProperty(base, property).getPropertyType();
            context.setPropertyResolved(true);
        }
        return result;
    }


    @Override
    public Object getValue(ELContext context, Object base, Object property) {
        if (context == null) {
            throw new NullPointerException();
        }
        Object result = null;
        if (isResolvable(base)) {
            Method method = toBeanProperty(base, property).getReadMethod();
            if (method == null) {
                throw new PropertyNotFoundException("Cannot read property " + property);
            }
            try {
                result = method.invoke(base);
            } catch (InvocationTargetException e) {

                throw new ELException(e.getTargetException());
            } catch (Exception e) {
                throw new ELException(e);
            }
            context.setPropertyResolved(true);
        }
        return result;
    }


    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        if (context == null) {
            throw new NullPointerException();
        }
        boolean result = readOnly;
        if (isResolvable(base)) {
            result |= toBeanProperty(base, property).isReadOnly();
            context.setPropertyResolved(true);
        }
        return result;
    }


    @Override
    public void setValue(ELContext context, Object base, Object property, Object value) {
        if (context == null) {
            throw new NullPointerException();
        }
        if (isResolvable(base)) {
            if (readOnly) {
                throw new PropertyNotWritableException("resolver is read-only");
            }
            Method method = toBeanProperty(base, property).getWriteMethod();
            if (method == null) {
                throw new PropertyNotWritableException("Cannot write property: " + property);
            }
            try {
                method.invoke(base, value);
            } catch (InvocationTargetException e) {
                throw new ELException("Cannot write property: " + property, e.getTargetException());
            } catch (IllegalAccessException e) {
                throw new PropertyNotWritableException("Cannot write property: " + property, e);
            }
            context.setPropertyResolved(true);
        }
    }

    @Override
    public Object invoke(ELContext context, Object base, Object method, Class<?>[] paramTypes, Object[] params) {
        if (context == null) {
            throw new NullPointerException();
        }
        Object result = null;
        if (isResolvable(base)) {
            if (params == null) {
                params = new Object[0];
            }
            String name = method.toString();
            Method target = findMethod(base, name, paramTypes, params.length);
            if (target == null) {
                throw new MethodNotFoundException("Cannot find method " + name + " with " + params.length + " parameters in " + base.getClass());
            }
            try {
                result = target.invoke(base, coerceParams(getExpressionFactory(context), target, params));
            } catch (InvocationTargetException e) {
                throw new ELException(e.getTargetException());
            } catch (IllegalAccessException e) {
                throw new ELException(e);
            }
            context.setPropertyResolved(true);
        }
        return result;
    }

    ;

    private Method findMethod(Object base, String name, Class<?>[] types, int paramCount) {
        if (types != null) {
            try {
                return findAccessibleMethod(base.getClass().getMethod(name, types));
            } catch (NoSuchMethodException e) {
                return null;
            }
        }
        Method varArgsMethod = null;
        for (Method method : base.getClass().getMethods()) {
            if (method.getName().equals(name)) {
                int formalParamCount = method.getParameterTypes().length;
                if (method.isVarArgs() && paramCount >= formalParamCount - 1) {
                    varArgsMethod = method;
                } else if (paramCount == formalParamCount) {
                    return findAccessibleMethod(method);
                }
            }
        }
        return varArgsMethod == null ? null : findAccessibleMethod(varArgsMethod);
    }

    private ExpressionFactory getExpressionFactory(ELContext context) {
        Object obj = context.getContext(ExpressionFactory.class);
        if (obj instanceof ExpressionFactory) {
            return (ExpressionFactory) obj;
        }
        if (defaultFactory == null) {
            defaultFactory = ExpressionFactory.newInstance();
        }
        return defaultFactory;
    }

    private Object[] coerceParams(ExpressionFactory factory, Method method, Object[] params) {
        Class<?>[] types = method.getParameterTypes();
        Object[] args = new Object[types.length];
        if (method.isVarArgs()) {
            int varargIndex = types.length - 1;
            if (params.length < varargIndex) {
                throw new ELException("Bad argument count");
            }
            for (int i = 0; i < varargIndex; i++) {
                coerceValue(args, i, factory, params[i], types[i]);
            }
            Class<?> varargType = types[varargIndex].getComponentType();
            int length = params.length - varargIndex;
            Object array = null;
            if (length == 1) {
                Object source = params[varargIndex];
                if (source != null && source.getClass().isArray()) {
                    if (types[varargIndex].isInstance(source)) {
                        array = source;
                    } else {
                        length = Array.getLength(source);
                        array = Array.newInstance(varargType, length);
                        for (int i = 0; i < length; i++) {
                            coerceValue(array, i, factory, Array.get(source, i), varargType);
                        }
                    }
                } else {
                    array = Array.newInstance(varargType, 1);
                    coerceValue(array, 0, factory, source, varargType);
                }
            } else {
                array = Array.newInstance(varargType, length);
                for (int i = 0; i < length; i++) {
                    coerceValue(array, i, factory, params[varargIndex + i], varargType);
                }
            }
            args[varargIndex] = array;
        } else {
            if (params.length != args.length) {
                throw new ELException("Bad argument count");
            }
            for (int i = 0; i < args.length; i++) {
                coerceValue(args, i, factory, params[i], types[i]);
            }
        }
        return args;
    }

    private void coerceValue(Object array, int index, ExpressionFactory factory, Object value, Class<?> type) {
        if (value != null || type.isPrimitive()) {
            Array.set(array, index, factory.coerceToType(value, type));
        }
    }


    private final boolean isResolvable(Object base) {
        return base != null;
    }


    private final BeanProperty toBeanProperty(Object base, Object property) {
        BeanProperties beanProperties = cache.get(base.getClass());
        if (beanProperties == null) {
            BeanProperties newBeanProperties = new BeanProperties(base.getClass());
            beanProperties = cache.putIfAbsent(base.getClass(), newBeanProperties);
            if (beanProperties == null) { // put succeeded, use new value
                beanProperties = newBeanProperties;
            }
        }
        BeanProperty beanProperty = property == null ? null : beanProperties.getBeanProperty(property.toString());
        if (beanProperty == null) {
            throw new PropertyNotFoundException("Could not find property " + property + " in " + base.getClass());
        }
        return beanProperty;
    }


    @SuppressWarnings("unused")
    private final void purgeBeanClasses(ClassLoader loader) {
        Iterator<Class<?>> classes = cache.keySet().iterator();
        while (classes.hasNext()) {
            if (loader == classes.next().getClassLoader()) {
                classes.remove();
            }
        }
    }
}
